﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml.Linq;

using Perseus;
using Perseus.Plugins;
using Perseus.Xml;

using Hetymine.Executioner.Controls;
using Hetymine.Executioner.Data;
using Hetymine.Executioner.Data.Console;
using Hetymine.Executioner.Plugins;
using Hetymine.Executioner.Triggers;

namespace Hetymine.Executioner.Commands {
    public class TriggerCommand : ICommand {
        private static TriggerCommandConfigurator _Configurator;

        public TriggerCommand() {
            this.Name = string.Empty;
            this.CallStrings = new List<string>();
            this.Commands = new List<CommandInfo>();            
            this.Triggers = new List<ITrigger>();
        }

        public List<CommandInfo> Commands { get; set; }
        public List<ITrigger> Triggers { get; set; }

        public bool HasTriggers {
            get {
                foreach (ITrigger t in this.Triggers) {
                    if (!t.IsEmpty) {
                        return true;
                    }
                }

                return false;
            }
        }

        #region ICommand Members
        public string Name { get; set; }        
        public bool IsEmpty {
            get {
                if (this.Commands.Count < 1) {
                    return true;
                }

                if (this.Commands.Count == 1 && this.Commands[0].IsEmpty) {
                    return true;
                }

                if (this.CallStrings.Count > 0) {
                    return false;
                }

                foreach (ITrigger trigger in this.Triggers) {
                    if (!trigger.IsEmpty) {
                        return false;
                    }
                }
                
                return true;
            }
        }

        public void Load(XElement element) {
            if (element.Attribute("KeepHistory") == null || element.Attribute("KeepHistory").Value == "partial") {
                this.KeepHistory = null;
            }
            else {
                this.KeepHistory = element.Attribute("KeepHistory").BoolOr(true);
            }

            this.Name = element.Element("Name").ValueOrEmpty();

            var commands = from c in element.Elements("Command")
                           select c;

            foreach (var c in commands) {
                this.Commands.Add(new CommandInfo(c));
            }

            if (element.Element("Triggers") != null) {
                var triggers = from t in element.Element("Triggers").Elements()
                               select t;

                foreach (var t in triggers) {
                    switch (t.Name.LocalName) {
                        case "CallString":                           
                            if (!t.Value.IsEmpty()) {
                                this.CallStrings.Add(t.Value);
                            }
                            break;
                        default:
                            try {
                                PluginInstance<IPlugin> plugin = Executioner.PluginService[t.Name.LocalName, typeof(TriggerPlugin)];
                                if (plugin != null) {
                                    TriggerPlugin tp = plugin.Instance as TriggerPlugin;
                                    if (tp.CanInitialize) {
                                        Hetymine.Executioner.Triggers.ITrigger trigger = tp.InitializeTrigger();
                                        trigger.Load(t);
                                        if (!trigger.IsEmpty) {
                                            this.Triggers.Add(trigger);
                                        }
                                    }
                                }
                            }
                            catch {
                                App.Log("Error loading " + t.Name.LocalName + " plugin trigger.");
                            }
                            break;
                    }
                }
            }

            // Call the trigger loaded function of the trigger plugin for each trigger
            var triggerPlugins = from tp in Executioner.PluginService.Plugins
                                 where tp.Instance is TriggerPlugin
                                 select tp;

            foreach (ITrigger trigger in this.Triggers) {
                foreach (PluginInstance<IPlugin> tp in triggerPlugins) {                    
                    if (trigger.GetType().FullName == tp.FullName + "Trigger") {
                        ((TriggerPlugin)tp.Instance).TriggerLoaded(trigger);
                    }
                }
            }
        }
        public XElement Save() {
            if (this.IsEmpty) {
                return null;
            }

            XElement xe = new XElement("Trigger");
            
            if (this.KeepHistory != null) {
                xe.Add(new XAttribute("KeepHistory", this.KeepHistory));
            }
            else {
                xe.Add(new XAttribute("KeepHistory", "partial"));
            }

            xe.Add(new XElement("Name", this.Name));

            foreach (CommandInfo ci in this.Commands) {
                XElement cmd = new XElement("Command", ci.Command);
                if (!ci.Arguments.IsEmpty()) {
                    cmd.Add(new XAttribute("Args", ci.Arguments));
                }
                xe.Add(cmd);
            }

            XElement triggers = new XElement("Triggers");
            
            foreach (string cs in this.CallStrings) {
                triggers.Add(new XElement("CallString", cs));
            }

            foreach (ITrigger t in this.Triggers) {
                XElement trigger = t.Save();
                if (trigger != null) {
                    triggers.Add(trigger);
                }
            }

            xe.Add(triggers);

            return xe;
        }

        public List<string> CallStrings { get; set; }
        public bool? KeepHistory { get; set; }
        public ExecutionResult Execute(string parameters) {
            return this.Execute(parameters, true);
        }
        public ExecutionResult Execute(string parameters, bool trigger) {
            List<string> commands = new List<string>();

            string command;
            string commandString;
            ArgumentInfo args;

            // Only directory commands except paramters            
            if (!parameters.IsEmpty()) {                
                ParameterInfo prams = new ParameterInfo(parameters);                

                // In case parameters is file surrounded in quotes
                if (prams.Count == 1) {
                    parameters = prams.Parameters[0];
                }

                bool validPath = !parameters.ContainsAny(Path.GetInvalidPathChars());
                bool validFile = File.Exists(parameters);
                bool used = false; // Have the parameters been used

                // If the parameter is a file with a matching extension callstring
                if (validFile) {
                    string ext = "*" + Path.GetExtension(parameters);
                    bool found = false;
                    foreach (string cs in this.CallStrings) {
                        if (cs.CharAt(0) == "*") {
                            string[] exts = cs.Split(";", SplitOptions.Trim);
                            if (exts.Contains(ext)) {
                                found = true;                                
                                break;
                            }
                        }
                    }

                    if (!found) {
                        validFile = false;
                    }

                    // Add back quotes since it is a file and should be 
                    // treated as one parameter
                    parameters = "\"" + parameters + "\"";
                }

                foreach (CommandInfo c in this.Commands) {
                    commandString = c.ToString();                
                    prams = new ParameterInfo(parameters);
                    args = new ArgumentInfo(c.Arguments);

                    // Treat parameter string as a single parameter if @raw specified
                    if (args.SpecialArguments.ContainsKey("raw")) {
                        int limit = 1;
                        if (args.SpecialArguments["raw"].Count > 0) {
                            if (!int.TryParse(args.SpecialArguments["raw"][0], out limit)) {
                                App.Log(
                                    "The @raw limit parameter, '" + args.SpecialArguments["raw"][0] + "', is invalid.", 
                                    MessageType.Notice
                                );
                            }
                            else if (limit < 1) {
                                App.Log(
                                    "The @raw limit parameter, '" + limit.ToString() + "', is out of bounds.",
                                    MessageType.Notice
                                );
                                limit = 1;
                            }
                        }
                        prams.SetToRaw(limit);
                    }

                    // Url encode parameters
                    if (args.SpecialArguments.ContainsKey("urlencode")) {
                        for (int i = 0; i < prams.Parameters.Count; ++i) {
                            prams.Parameters[i] = HttpUtility.UrlEncode(prams.Parameters[i]);
                        }
                    }

                    // If parameters can be inserted then use that result
                    command = prams.InsertInto(commandString);
                    if (command != commandString) {
                        used = true;
                        commands.Add(command);
                        
                        continue;
                    }

                    // If the command is a directory, then append arguments to it
                    if (Directory.Exists(c.Command) && validPath) {
                        used = true;

                        command = "\"" + Path.Combine(c.Command, parameters) + "\"";
                        command += " " + c.Arguments;                        
                        commands.Add(command);

                        continue;
                    }

                    // If the command is a url, then append arguments to it
                    Uri uri;
                    if (Uri.TryCreate(c.Command, UriKind.RelativeOrAbsolute, out uri) &&
                        (parameters.StartsWith("/", StringComparison.Ordinal) ||
                        parameters.StartsWith("\\", StringComparison.Ordinal)) ||
                        parameters.StartsWith("?", StringComparison.Ordinal) ||
                        parameters.StartsWith("#", StringComparison.Ordinal)
                    ) {
                        string type = parameters.Substring(0, 1);
                        parameters = parameters.Substring(1);

                        used = true;

                        string url = uri.Scheme + "://" + uri.Host + uri.AbsolutePath;
                        string query = uri.Query;
                        if (query.Is("?")) {
                            query = string.Empty;
                        }
                        string fragment = uri.Fragment;
                        if (fragment.Is("#")) {
                            fragment = string.Empty;
                        }

                        if (type.Is("?")) {
                            if (query.IsEmpty()) {
                                query = "?" + parameters;
                            }
                            else {
                                query += "&" + parameters;
                            }
                        }
                        else if (type.Is("#")) {
                            if (fragment.IsEmpty()) {
                                fragment = "#" + parameters;
                            }
                            // If it's a hashbang, join like a path
                            else if (fragment.StartsWith("#!", StringComparison.Ordinal)) {
                                if (fragment.EndsWith("/", StringComparison.Ordinal) || 
                                    fragment.EndsWith("\\", StringComparison.Ordinal)
                                ) {
                                    fragment = fragment.Substring(0, fragment.Length - 1);
                                }
                                
                                if (parameters.StartsWith("/", StringComparison.Ordinal) ||
                                    parameters.StartsWith("\\", StringComparison.Ordinal)
                                ) {
                                    parameters = parameters.Substring(1);
                                }

                                // Url edge cases hurray!
                                if (fragment == "#!") {
                                    fragment += parameters;
                                }
                                else {
                                    fragment += "/" + parameters;
                                }
                            }
                            else { // Just replace it I guess lol hashes are stupid!
                                fragment = "#" + parameters;
                            }
                        }
                        else { // Treat as path
                            if (url.EndsWith("/", StringComparison.Ordinal) ||
                                url.EndsWith("\\", StringComparison.Ordinal)
                            ) {
                                url = url.Substring(0, url.Length - 1);
                            }

                            url += "/" + parameters;
                        }

                        command = "\"" + url + query + fragment + "\"";                        
                        command += " " + c.Arguments;
                        commands.Add(command);

                        continue;
                    }                    
                    
                    // Finally just add the regular command string
                    commands.Add(commandString);
                }

                // Parameters are not used so do not execute command
                if (!used) {
                    return ExecutionResult.None;
                }
            }
            else {
                // TODO: Some sort of syntax to allow commands to be 
                // stripped without returning false like "google.ca{/search?q={0}}"
                foreach (CommandInfo c in this.Commands) {
                    commandString = c.ToString();
                    command = ParameterInfo.RemoveInsertArguments(commandString);

                    if (commandString.IsNot(command)) {
                        return ExecutionResult.None;
                    }

                    commands.Add(command);
                }
            }
            
            ExecutionResult result = ExecutionResult.Success;

            int count = Executioner._ExecutionCount;

            bool keepHistory = true;
            if (this.KeepHistory == false || (this.KeepHistory == null && trigger)) {
                keepHistory = false;
            }

            foreach (string c in commands) {
                // We ensure each command has it's own max executioner count
                Executioner._ExecutionCount = count;
                
                if (!Executioner.ExecuteCommand(c, string.Empty, keepHistory)) {
                    result = ExecutionResult.Failure;
                }
            }

            Executioner._ExecutionCount = count;

            return result;
        }

        public ICommandConfigurator<ICommand> Configurator {
            get {
                if (TriggerCommand._Configurator == null) {
                    TriggerCommand._Configurator = new TriggerCommandConfigurator();
                }
                TriggerCommand._Configurator.InitializeConfig(this);
                return TriggerCommand._Configurator;
            }
        }

        public object Clone() {
            TriggerCommand tc = (TriggerCommand)this.MemberwiseClone();

            tc.CallStrings = new List<string>();
            foreach (string cs in this.CallStrings) {
                tc.CallStrings.Add(cs);
            }

            tc.Commands = new List<CommandInfo>();
            foreach (CommandInfo ci in this.Commands) {
                tc.Commands.Add(new CommandInfo(ci.Command, ci.Arguments));
            }

            tc.Triggers = new List<ITrigger>();
            foreach (ITrigger t in this.Triggers) {
                tc.Triggers.Add((ITrigger)t.Clone());
            }

            return tc;            
        }
        #endregion
    }   
}
